package eu.larkc.plugin.transform;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.List;
import java.util.Random;

import javax.tools.JavaCompiler;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;

import org.apache.tools.ant.Location;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.Java;
import org.apache.tools.ant.types.FileList;
import org.apache.tools.ant.types.Path;
import org.jdom.JDOMException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import translators.ClassFilter;
import dfdl.DFDLBuildSchema;
import dfdl.exception.DFDLException;

/**
 * Utility Class for Defuddle
 * 
 * @author Ying Jiang
 * 
 */
public class DefuddleUtil {

	private static final Logger logger = LoggerFactory
			.getLogger(DefuddleUtil.class);

	// We want the package names to be 10 letter characters.
	private static final String letterChar = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
	private static final int length = 10;

	/**
	 * Defuddle will generate Java source code files and then compile them into
	 * .class files for DFDL transformation. This method can generate package
	 * name for the Java source code.
	 * 
	 * @return a generated package name
	 */
	public static String generatePackageName() {
		StringBuffer sb = new StringBuffer();
		Random random = new Random();
		for (int i = 0; i < length; i++) {
			sb.append(letterChar.charAt(random.nextInt(letterChar.length())));
		}
		return sb.toString() + "Pkg";
	}

	/**
	 * Perform DFDL transforming.
	 * 
	 * @param schema
	 * @param data
	 * @param dfdlpackagename
	 * @param baseDir
	 * @param buildPath
	 * @param buildDir
	 * @param libDir
	 * @param xslRdf
	 * @return
	 * @throws IOException
	 * @throws DFDLException
	 */
	public static byte[] DFDLTransform(String schema, String data,
			String dfdlpackagename, String baseDir, String buildPath,
			String buildDir, String libDir, String xslRdf) throws IOException,
			DFDLException {
		InputStream dataIn = new FileInputStream(data);
		InputStream schemaFile = new FileInputStream(schema);

		File homeDir = new File(baseDir + "/" + buildPath);
		if (!homeDir.exists()) {
			homeDir.mkdir();
		}
		File buildsDir = new File(homeDir.getPath() + "/" + buildDir);
		if (!buildsDir.exists()) {
			buildsDir.mkdir();
		}
		String classesLocation = buildsDir.getPath() + "/classes";

		String folderName = "tmp";

		File classesDir = new File(classesLocation);
		File dataDir = new File(classesLocation + "/" + folderName);

		if (!classesDir.exists()) {
			classesDir.mkdir();
		}
		if (!dataDir.exists()) {
			dataDir.mkdir();
		}

		File dataOut = new File(classesLocation + "/" + folderName + "/"
				+ dfdlpackagename + "Data.dat");

		cleanOldDataFiles(dataOut.getParentFile(), dataOut.getName());

		if (!classesDir.exists()) {
			classesDir.mkdir();
		}
		if (!dataDir.exists()) {
			dataDir.mkdir();
		}

		long timestamp = Calendar.getInstance().getTimeInMillis();
		dataOut = new File(dataOut.getAbsolutePath() + "." + timestamp);
		if (!dataOut.exists()) {
			dataOut.createNewFile();
		}

		FileOutputStream dataFileOut = new FileOutputStream(
				dataOut.getAbsoluteFile());
		while (dataIn.available() > 0) {
			int available = dataIn.available();
			byte[] b;
			if (available >= 1000)
				b = new byte[1000];
			else {
				b = new byte[available];
			}
			dataIn.read(b);
			dataFileOut.write(b);
		}
		try {
			dataIn.close();
			dataFileOut.close();
		} catch (IOException e) {
			e.printStackTrace();
		}

		return transform(schemaFile, dataOut.getCanonicalPath(),
				dfdlpackagename, baseDir, buildPath, buildDir, libDir, xslRdf);
	}

	private static byte[] transform(InputStream schemaFile,
			String dataLocation, String dfdlpackagename, String baseDir,
			String buildPath, String buildDir, String libDirStr, String xslRdf)
			throws IOException, DFDLException {

		dfdlpackagename = dfdlpackagename.replaceAll("\\.", "/");

		File homeDir = new File(baseDir + "/" + buildPath);
		File buildsDir = new File(homeDir.getPath() + "/" + buildDir);
		File libDir = new File(baseDir + "/" + libDirStr);

		if (!buildsDir.exists()) {
			buildsDir.mkdir();
		}
		String classesLocation = buildsDir.getPath() + "/classes";
		String packageLocation = buildsDir.getPath() + "/classes/"
				+ dfdlpackagename;
		String classPath = " " + buildPath + "/" + buildDir + "/classes";
		String compileCP = ".";
		String schemaDir = buildsDir.getPath() + "/schemas";
		if ((libDir.exists()) && (libDir.isDirectory())) {
			File[] files = libDir.listFiles();
			for (int i = 0; i < files.length; ++i) {
				if (files[i].getName().endsWith(".jar")) {
					classPath = classPath + ", " + libDirStr + "/"
							+ files[i].getName();
					compileCP += ";" + libDirStr + "/" + files[i].getName();
				}

			}
		} else {
			logger.error("Could not find lib directory "
					+ libDir.getAbsolutePath()
					+ " classpath may be incomplete.");
		}

		byte[] result = (byte[]) null;

		File schemaLocDir = new File(schemaDir);
		File packageLocDir = new File(packageLocation);
		File classesLocDir = new File(classesLocation);
		Project proj = new Project();
		boolean shouldBuild = false;
		String schemaPath = schemaLocDir.getPath() + "/" + dfdlpackagename
				+ "Schema.xsd";
		if ((schemaLocDir.exists()) && (packageLocDir.exists())) {
			File oldSchema = new File(schemaPath);

			if (packageLocDir.listFiles(new ClassFilter()).length == 0) {
				shouldBuild = true;
			} else if (oldSchema.exists()) {
				try {
					writeFile(schemaPath + ".tmp", schemaFile);
					File tempSchemaFile = new File(schemaPath + ".tmp");
					InputStream oldIn = new FileInputStream(oldSchema);

					InputStream newIn = new FileInputStream(schemaPath + ".tmp");

					if (!gov.pnnl.sdg.xmlcompare.filecompare.compare(oldIn,
							newIn)) {
						shouldBuild = true;
						oldSchema.delete();
						tempSchemaFile.renameTo(oldSchema);
					} else {
						tempSchemaFile.delete();
					}
					oldIn.close();
					newIn.close();
				} catch (JDOMException e) {
					shouldBuild = true;
				} catch (IOException e) {
					shouldBuild = true;
				}
			} else {
				shouldBuild = true;

				writeFile(schemaPath, schemaFile);
			}
		} else {
			shouldBuild = true;
			schemaLocDir.mkdir();
			writeFile(schemaPath, schemaFile);
		}

		if (!classesLocDir.exists()) {
			classesLocDir.mkdir();
		}
		removeFiles(packageLocDir);
		packageLocDir.mkdir();

		DFDLBuildSchema builder = new DFDLBuildSchema();
		InputStream dfdlSchema = new FileInputStream(schemaPath);

		builder.performBuild(dfdlSchema, classesLocation, dfdlpackagename);

		List<File> files = getAllSourceFiles(new File(classesLocation),
				dfdlpackagename);

		JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
		StandardJavaFileManager fileManager = compiler.getStandardFileManager(
				null, null, null);

		List<String> options = new ArrayList<String>();
		options.addAll(Arrays.asList("-classpath", compileCP));

		options.add("-d");
		options.add(classesLocDir.getAbsolutePath());
		options.add("-Xlint:none");
		options.add("-nowarn");

		Iterable compilationUnits = fileManager
				.getJavaFileObjectsFromFiles(files);

		if (!compiler
				.getTask(null, fileManager, null, options, null,
						compilationUnits).call().booleanValue()) {
			logger.error("Compilation failed");
		}

		fileManager.close();
		try {
			Java java = new Java();
			java.setDir(new File(baseDir));
			java.setLocation(new Location(baseDir));

			java.setClassname("dfdl.DFDLTestSchema");
			java.setTaskName("performTest");
			java.setFork(true);
			Path cp = java.createClasspath();
			FileList classpathfiles = new FileList();
			classpathfiles.setDir(new File(baseDir));
			classpathfiles.setFiles(classPath);

			cp.addFilelist(classpathfiles);
			java.setClasspath(cp);

			proj.setCoreLoader(ClassLoader.getSystemClassLoader());
			java.setProject(proj);
			java.setFailonerror(true);

			String folderName = dfdlpackagename;
			if (folderName.indexOf(".") >= 0)
				folderName = folderName.substring(0, folderName.indexOf("."));
			File outputOut = new File(classesLocation + "/" + folderName + "/"
					+ dfdlpackagename + "Output.xml");
			outputOut.createNewFile();

			File dataFile = new File(dataLocation);

			if ((xslRdf != null) && (xslRdf.length() > 0)) {

				File xslRDFFile = new File(xslRdf);
				String xslRdfAbsolutePath = "file:///"
						+ xslRDFFile.getAbsolutePath()
								.replaceAll("[\\\\]", "/");
				logger.info("GRDDL xslt: " + xslRdfAbsolutePath);
				java.setArgs(dfdlpackagename + " \"" + dataFile.toURI()
						+ "\" \"" + outputOut.toURI() + "\"" + " --xsl-rdf "
						+ "\"" + xslRdfAbsolutePath + "\"");
			} else {
				java.setArgs(dfdlpackagename + " \"" + dataFile.toURI()
						+ "\" \"" + outputOut.toURI() + "\"");
			}

			String mx = System.getenv("JARGS");

			if ((mx != null) && (mx != "")) {
				java.setJvmargs(mx);
			}

			ByteArrayOutputStream output = new ByteArrayOutputStream();
			java.execute();

			FileInputStream fis = new FileInputStream(
					outputOut.getAbsoluteFile());
			while (fis.available() > 0) {
				int available = fis.available();
				byte[] b;
				if (available >= 1000)
					b = new byte[1000];
				else {
					b = new byte[available];
				}
				fis.read(b);
				output.write(b);
			}
			fis.close();
			output.close();
			result = output.toByteArray();
		} catch (Throwable t) {
			logger.error("Error occured: see defuddle.log for details");

			t.printStackTrace();

			DFDLException de = new DFDLException();
			de.setStackTrace(t.getStackTrace());
			throw de;
		}

		return result;
	}

	private static void removeFiles(File fileToRemove) {
		if (fileToRemove.exists()) {
			if (fileToRemove.isDirectory()) {
				File[] children = fileToRemove.listFiles();
				if (children != null) {
					for (int i = 0; i < children.length; ++i) {
						if (children[i].isDirectory())
							removeFiles(children[i]);
						else
							children[i].delete();
					}
				}
			}
			fileToRemove.delete();
		}
	}

	private static String writeFile(String path, InputStream fileStream)
			throws IOException, FileNotFoundException {
		if (fileStream.available() > 0) {
			File schemaOut = new File(path);
			if (!schemaOut.exists()) {
				schemaOut.createNewFile();
			}
			FileOutputStream dfdlFileOut = new FileOutputStream(schemaOut);
			while (fileStream.available() > 0) {
				int available = fileStream.available();
				byte[] b;
				if (available >= 1000)
					b = new byte[1000];
				else {
					b = new byte[available];
				}
				fileStream.read(b);
				dfdlFileOut.write(b);
			}
			dfdlFileOut.close();
			return schemaOut.getAbsolutePath();
		}
		return null;
	}

	private static void cleanOldDataFiles(File directory, String dataName)
			throws IOException, FileNotFoundException {
		if ((directory.exists()) && (directory.isDirectory())) {
			File[] children = directory.listFiles();
			if (children != null)
				for (int i = 0; i < children.length; ++i) {
					if ((children[i].isDirectory())
							|| (!children[i].getName().startsWith(dataName)))
						continue;
					children[i].delete();
				}
		}
	}

	private static List<File> getAllFiles(File location, String extension,
			ArrayList<File> list, String prefix) {
		for (File f : location.listFiles()) {
			if (f.isDirectory())
				getAllFiles(f, extension, list, prefix);
			else if ((f.getName().endsWith(extension))
					&& (f.getAbsolutePath().contains(prefix)))
				list.add(f);
		}
		return list;
	}

	public static List<File> getAllSourceFiles(File location, String prefix) {
		return getAllFiles(location, ".java", new ArrayList<File>(), prefix);
	}
}
